import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import json
import math
import os
import zipfile

# 尝试导入 tkinterdnd2，如果不可用则禁用拖拽功能
try:
    from tkinterdnd2 import DND_FILES, TkinterDnD
    HAS_DND = True
except ImportError:
    HAS_DND = False
    print("警告: tkinterdnd2 库未安装，拖拽功能将被禁用。")
    print("您可以通过运行 'pip install tkinterdnd2' 来安装它。")

class MalodyToOsuConverter:
    def __init__(self, root):
        """
        初始化主窗口和变量，构建界面。
        """
        self.root = root
        self.root.title("Malody to osu!mania Converter")
        self.root.geometry("700x600")
        
        # 变量定义
        self.malody_file_path = tk.StringVar()  # Malody文件路径
        self.output_file_path = tk.StringVar()  # 输出文件路径
        self.conversion_status = tk.StringVar(value="等待转换 - 使用浏览按钮选择文件")
        
        # 新增变量：导出类型和资源文件路径
        self.export_type = tk.StringVar(value="osu")  # 默认导出.osu文件
        self.background_file_path = tk.StringVar()    # 背景图片路径
        self.audio_file_path = tk.StringVar()         # 音频文件路径
        
        # 创建UI界面
        self.create_widgets()
        
    def create_widgets(self):
        """
        构建主界面，包括拖拽区、文件选择、导出选项、日志等。
        """
        # Main frame
        main_frame = ttk.Frame(self.root, padding="20")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 拖拽提示 (仅在支持拖拽时显示)
        if HAS_DND:
            drop_label = ttk.Label(main_frame, 
                                  text="拖拽 Malody 文件 (.mc) 到下方区域",
                                  font=("Arial", 10, "bold"))
            drop_label.grid(row=0, column=0, columnspan=3, pady=10)
            
            # 拖拽区域
            drop_frame = ttk.LabelFrame(main_frame, text="拖拽区域", padding="10")
            drop_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
            drop_frame.columnconfigure(0, weight=1)
            
            # 设置拖拽区域接受拖拽
            drop_frame.drop_target_register(DND_FILES)
            drop_frame.dnd_bind('<<Drop>>', self.on_drop)
            
            # 拖拽图标和提示
            drop_icon = ttk.Label(drop_frame, text="📁", font=("Arial", 24))
            drop_icon.grid(row=0, column=0, pady=5)
            
            drop_text = ttk.Label(drop_frame, text="将 Malody 文件拖拽到此处", font=("Arial", 10))
            drop_text.grid(row=1, column=0, pady=5)
            
            # 分隔线
            separator = ttk.Separator(main_frame, orient='horizontal')
            separator.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
            
            # 传统文件选择方式提示
            ttk.Label(main_frame, text="或使用传统方式选择文件:").grid(row=3, column=0, sticky=tk.W, pady=5)
        else:
            # 如果不支持拖拽，直接显示文件选择提示
            ttk.Label(main_frame, text="选择 Malody 文件 (.mc):").grid(row=0, column=0, sticky=tk.W, pady=5)
        
        # Input file selection
        row_offset = 4 if HAS_DND else 1
        ttk.Label(main_frame, text="Malody 文件 (.mc):").grid(row=row_offset, column=0, sticky=tk.W, pady=5)
        ttk.Entry(main_frame, textvariable=self.malody_file_path, width=50).grid(row=row_offset, column=1, padx=5, pady=5)
        ttk.Button(main_frame, text="浏览", command=self.browse_malody_file).grid(row=row_offset, column=2, padx=5, pady=5)
        
        # Output file selection
        ttk.Label(main_frame, text="输出 osu! 文件 (.osu):").grid(row=row_offset+1, column=0, sticky=tk.W, pady=5)
        ttk.Entry(main_frame, textvariable=self.output_file_path, width=50).grid(row=row_offset+1, column=1, padx=5, pady=5)
        ttk.Button(main_frame, text="浏览", command=self.browse_output_file).grid(row=row_offset+1, column=2, padx=5, pady=5)
        
        # 新增：导出类型选择区域
        export_frame = ttk.LabelFrame(main_frame, text="导出选项", padding="10")
        export_frame.grid(row=row_offset+2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
        
        # 创建单选按钮
        ttk.Radiobutton(export_frame, text="只导出 .osu 文件", variable=self.export_type, 
                       value="osu", command=self.toggle_export_type).grid(row=0, column=0, sticky=tk.W, padx=5)
        ttk.Radiobutton(export_frame, text="导出 .osz 压缩包", variable=self.export_type, 
                       value="osz", command=self.toggle_export_type).grid(row=0, column=1, sticky=tk.W, padx=5)
        
        # 资源文件选择区域（初始状态为禁用）
        self.resources_frame = ttk.Frame(export_frame)
        self.resources_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
        self.resources_frame.grid_remove()  # 初始隐藏
        
        ttk.Label(self.resources_frame, text="背景图片 (.png/.jpg):").grid(row=0, column=0, sticky=tk.W, pady=5)
        ttk.Entry(self.resources_frame, textvariable=self.background_file_path, width=40, state="disabled").grid(row=0, column=1, padx=5, pady=5)
        ttk.Button(self.resources_frame, text="浏览", command=self.browse_background_file, state="disabled").grid(row=0, column=2, padx=5, pady=5)
        
        ttk.Label(self.resources_frame, text="音频文件 (.ogg):").grid(row=1, column=0, sticky=tk.W, pady=5)
        ttk.Entry(self.resources_frame, textvariable=self.audio_file_path, width=40, state="disabled").grid(row=1, column=1, padx=5, pady=5)
        ttk.Button(self.resources_frame, text="浏览", command=self.browse_audio_file, state="disabled").grid(row=1, column=2, padx=5, pady=5)
        
        # Conversion button
        ttk.Button(main_frame, text="开始转换", command=self.convert).grid(row=row_offset+3, column=1, pady=20)
        
        # Status
        ttk.Label(main_frame, text="状态:").grid(row=row_offset+4, column=0, sticky=tk.W, pady=5)
        ttk.Label(main_frame, textvariable=self.conversion_status).grid(row=row_offset+4, column=1, sticky=tk.W, pady=5)
        
        # Progress bar
        self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
        self.progress.grid(row=row_offset+5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
        
        # Log area
        ttk.Label(main_frame, text="转换日志:").grid(row=row_offset+6, column=0, sticky=tk.W, pady=5)
        
        # 创建日志框架和滚动条
        log_frame = ttk.Frame(main_frame)
        log_frame.grid(row=row_offset+7, column=0, columnspan=3, pady=5, sticky=(tk.W, tk.E, tk.N, tk.S))
        log_frame.columnconfigure(0, weight=1)
        log_frame.rowconfigure(0, weight=1)
        
        self.log_text = tk.Text(log_frame, height=10, width=80)
        scrollbar = ttk.Scrollbar(log_frame, orient="vertical", command=self.log_text.yview)
        self.log_text.configure(yscrollcommand=scrollbar.set)
        
        self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        
        # Configure grid weights
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(row_offset+7, weight=1)
        log_frame.columnconfigure(0, weight=1)
        log_frame.rowconfigure(0, weight=1)
        
    def toggle_export_type(self):
        """
        根据选择的导出类型显示或隐藏资源文件选择区域。
        选择osz时启用背景和音频文件选择，否则禁用。
        """
        if self.export_type.get() == "osz":
            self.resources_frame.grid()  # 显示资源文件选择区域
            # 启用资源文件选择按钮和输入框
            for widget in self.resources_frame.winfo_children():
                if isinstance(widget, ttk.Entry):
                    widget.configure(state="normal")
                elif isinstance(widget, ttk.Button):
                    widget.configure(state="normal")
            self.log_message("已选择导出 .osz 压缩包，请选择背景图片和音频文件")
        else:
            self.resources_frame.grid_remove()  # 隐藏资源文件选择区域
            # 禁用资源文件选择按钮和输入框
            for widget in self.resources_frame.winfo_children():
                if isinstance(widget, ttk.Entry):
                    widget.configure(state="disabled")
                elif isinstance(widget, ttk.Button):
                    widget.configure(state="disabled")
            self.log_message("已选择只导出 .osu 文件")
        
    def on_drop(self, event):
        """
        处理拖拽事件，将拖入的文件路径填入输入框，并自动设置输出路径。
        仅支持.mc文件。
        """
        if event.data:
            # 提取文件路径（处理Tkinter拖拽事件的数据格式）
            files = self.root.tk.splitlist(event.data)
            if files:
                file_path = files[0]
                if file_path.lower().endswith('.mc'):
                    self.malody_file_path.set(file_path)
                    # 自动设置输出路径
                    if not self.output_file_path.get():
                        base_name = file_path.rsplit('.', 1)[0]
                        if self.export_type.get() == "osz":
                            self.output_file_path.set(base_name + "_converted.osz")
                        else:
                            self.output_file_path.set(base_name + "_converted.osu")
                    self.log_message(f"已加载文件: {os.path.basename(file_path)}")
                else:
                    self.log_message("错误: 请拖拽 .mc 格式的文件")
        
    def browse_malody_file(self):
        """
        浏览选择Malody文件，并自动建议输出路径。
        """
        file_path = filedialog.askopenfilename(
            title="选择 Malody 文件",
            filetypes=[("Malody files", "*.mc"), ("All files", "*.*")]
        )
        if file_path:
            self.malody_file_path.set(file_path)
            # Suggest output path
            if not self.output_file_path.get():
                base_name = file_path.rsplit('.', 1)[0]
                if self.export_type.get() == "osz":
                    self.output_file_path.set(base_name + "_converted.osz")
                else:
                    self.output_file_path.set(base_name + "_converted.osu")
            self.log_message(f"已选择文件: {os.path.basename(file_path)}")
    
    def browse_output_file(self):
        """
        浏览选择输出文件路径，根据导出类型自动设置扩展名。
        """
        if self.export_type.get() == "osz":
            file_types = [("osu! 压缩包", "*.osz"), ("All files", "*.*")]
            def_ext = ".osz"
        else:
            file_types = [("osu! files", "*.osu"), ("All files", "*.*")]
            def_ext = ".osu"
            
        file_path = filedialog.asksaveasfilename(
            title="保存输出文件",
            defaultextension=def_ext,
            filetypes=file_types
        )
        if file_path:
            self.output_file_path.set(file_path)
            self.log_message(f"输出路径设置为: {os.path.basename(file_path)}")
    
    def browse_background_file(self):
        """
        浏览选择背景图片文件（png/jpg）。
        """
        file_path = filedialog.askopenfilename(
            title="选择背景图片",
            filetypes=[("PNG 图片", "*.png"), ("JPEG 图片", "*.jpg"), ("All files", "*.*")]
        )
        if file_path:
            self.background_file_path.set(file_path)
            self.log_message(f"已选择背景图片: {os.path.basename(file_path)}")
    
    def browse_audio_file(self):
        """
        浏览选择音频文件（ogg）。
        """
        file_path = filedialog.askopenfilename(
            title="选择音频文件",
            filetypes=[("OGG 音频", "*.ogg"), ("All files", "*.*")]
        )
        if file_path:
            self.audio_file_path.set(file_path)
            self.log_message(f"已选择音频文件: {os.path.basename(file_path)}")
    
    def log_message(self, message):
        """
        在日志区域输出信息，并自动滚动到底部。
        """
        self.log_text.insert(tk.END, message + "\n")
        self.log_text.see(tk.END)
        self.root.update_idletasks()

    def convert(self):
        """
        主转换流程：
        1. 检查输入输出路径和资源文件（如导出osz）。
        2. 读取Malody文件，转换为osu格式。
        3. 根据导出类型写入.osu或.osz文件。
        4. 日志和异常处理。
        """
        malody_path = self.malody_file_path.get()
        output_path = self.output_file_path.get()
        
        if not malody_path or not output_path:
            messagebox.showerror("错误", "请选择输入和输出文件路径")
            return
        
        # 如果选择导出osz，检查资源文件是否选择
        if self.export_type.get() == "osz":
            if not self.background_file_path.get() or not self.audio_file_path.get():
                messagebox.showerror("错误", "导出.osz压缩包需要选择背景图片和音频文件")
                return
        
        try:
            self.progress.start()
            self.conversion_status.set("转换中...")
            self.log_message("开始转换过程")
            
            # Read Malody file
            self.log_message("读取 Malody 文件...")
            with open(malody_path, 'r', encoding='utf-8') as f:
                malody_data = json.load(f)
            
            # Convert to osu format
            self.log_message("转换数据格式...")
            osu_content = self.convert_to_osu(malody_data)
            
            # 根据导出类型执行不同的操作
            if self.export_type.get() == "osz":
                # 先创建临时.osu文件
                temp_osu_path = output_path.replace('.osz', '.osu')
                self.log_message("写入临时 .osu 文件...")
                with open(temp_osu_path, 'w', encoding='utf-8') as f:
                    f.write(osu_content)
                
                # 创建.osz压缩包
                self.log_message("创建 .osz 压缩包...")
                self.create_osz_file(temp_osu_path, self.audio_file_path.get(), 
                                    self.background_file_path.get(), output_path)
                
                # 删除临时.osu文件
                os.remove(temp_osu_path)
                self.log_message("临时文件已清理")
            else:
                # 直接写入.osu文件
                self.log_message("写入 .osu 文件...")
                with open(output_path, 'w', encoding='utf-8') as f:
                    f.write(osu_content)
            
            self.conversion_status.set("转换完成")
            self.log_message("转换成功完成!")
            self.log_message(f"输出文件: {output_path}")
            messagebox.showinfo("成功", "转换完成!")
            
            # 新增：打开输出文件夹
            folder = os.path.dirname(output_path)
            if folder and os.path.exists(folder):
                try:
                    os.startfile(folder)
                except Exception as e:
                    self.log_message(f"无法自动打开文件夹: {e}")
        
        except Exception as e:
            self.conversion_status.set("转换失败")
            self.log_message(f"错误: {str(e)}")
            messagebox.showerror("错误", f"转换过程中发生错误: {str(e)}")
        finally:
            self.progress.stop()
    
    def create_osz_file(self, osu_file_path, audio_path, background_path, output_osz_path):
        """
        创建osz压缩包，将.osu、音频和背景图片打包。
        """
        with zipfile.ZipFile(output_osz_path, 'w', zipfile.ZIP_DEFLATED) as osz_zip:
            # 添加 .osu 文件
            osz_zip.write(osu_file_path, os.path.basename(osu_file_path))
            
            # 添加音频文件
            if os.path.exists(audio_path):
                osz_zip.write(audio_path, os.path.basename(audio_path))
                self.log_message(f"已添加音频文件: {os.path.basename(audio_path)}")
            else:
                self.log_message("警告: 音频文件不存在，跳过添加")
            
            # 添加背景图
            if os.path.exists(background_path):
                osz_zip.write(background_path, os.path.basename(background_path))
                self.log_message(f"已添加背景图片: {os.path.basename(background_path)}")
            else:
                self.log_message("警告: 背景图片不存在，跳过添加")

        self.log_message(f"已创建 osz 文件: {output_osz_path}")
    
    def convert_to_osu(self, malody_data):
        """
        将Malody谱面数据转换为osu!mania格式字符串。
        包括元数据、难度、事件、TimingPoints和HitObjects等部分。
        """
        # 提取元数据
        meta = malody_data.get('meta', {})
        song = meta.get('song', {})
        
        title = song.get('title', 'Unknown Title')
        title_unicode = song.get('title', 'Unknown Title')
        artist = song.get('artist', 'Unknown Artist')
        artist_unicode = song.get('artist', 'Unknown Artist')
        creator = meta.get('creator', 'Unknown Creator')
        version = meta.get('version', 'Unknown Version')
        mode = meta.get('mode', 3)  # 3 is osu!mania
        
        # 获取时间点和音符
        time = malody_data.get('time', [])
        notes = malody_data.get('note', [])

        # 计算全局offset（如果最后一个note是offset占位）
        global_offset_ms = 0
        if notes and (notes[-1].get('type') == 'offset' or notes[-1].get('sound') == 'offset' or notes[-1].get('column', -1) == -1):
            # 你可以根据实际导出的Malody文件结构判断最后一个note的类型
            last_note = notes[-1]
            beat = last_note.get('beat', [0, 0, 1])
            bar, numerator, denominator = beat
            # 复用 beat_to_ms 计算
            def beat_to_ms(bar, numerator, denominator):
                if not time_points:
                    return int((bar + numerator / denominator) * 60000 / 120)
                for i in range(len(time_points)-1, -1, -1):
                    if bar + numerator / denominator >= time_points[i]['bar']:
                        tp = time_points[i]
                        break
                else:
                    tp = time_points[0]
                beats_since_tp = (bar + numerator / denominator) - tp['bar']
                ms = tp['offset'] + beats_since_tp * 60000 / tp['bpm']
                return int(ms)
            # 构建 time_points
            time_points = []
            for t in time:
                t_offset = t.get('offset', 0)
                t_bpm = t.get('bpm', 120)
                t_bar = t.get('bar', 0)
                time_points.append({'bar': t_bar, 'offset': t_offset, 'bpm': t_bpm})
            global_offset_ms = beat_to_ms(bar, numerator, denominator)
            bpm = time[-1].get('bpm', 120) if time else 120
            global_offset_ms += 60000 / bpm
            # 移除最后一个note
            notes = notes[:-1]
        
        # 计算BPM和offset
        bpms = []
        if time:
            first_point = time[0]
            bpm = first_point.get('bpm', 120)
            offset = first_point.get('offset', 0)
            bpms.append((offset, bpm))
        
        # 生成osu文件内容
        content = []
        
        # Header部分
        content.append("osu file format v14")
        content.append("")
        
        # General部分
        content.append("[General]")
        # 使用用户选择的音频文件名
        audio_filename = os.path.basename(self.audio_file_path.get()) if self.audio_file_path.get() else "audio.ogg"
        content.append(f"AudioFilename: {audio_filename}")
        content.append("AudioLeadIn: 0")
        content.append("PreviewTime: -1")
        content.append("Countdown: 0")
        content.append("SampleSet: Soft")
        content.append("StackLeniency: 0.7")
        content.append("Mode: 3")
        content.append("LetterboxInBreaks: 0")
        content.append("SpecialStyle: 0")
        content.append("WidescreenStoryboard: 0")
        content.append("")
        
        # Editor部分
        content.append("[Editor]")
        content.append("DistanceSpacing: 1.2")
        content.append("BeatDivisor: 4")
        content.append("GridSize: 4")
        content.append("TimelineZoom: 1")
        content.append("")
        
        # Metadata部分
        content.append("[Metadata]")
        content.append(f"Title:{title}")
        content.append(f"TitleUnicode:{title_unicode}")
        content.append(f"Artist:{artist}")
        content.append(f"ArtistUnicode:{artist_unicode}")
        content.append(f"Creator:{creator}")
        content.append(f"Version:{version}")
        content.append("Source:")
        content.append("Tags:malody converted")
        content.append("BeatmapID:0")
        content.append("BeatmapSetID:-1")
        content.append("")
        
        # Difficulty部分
        content.append("[Difficulty]")
        content.append("HPDrainRate:7")
        # 自动设置键数
        key_count = 4  # 默认4K
        # 优先从 mode_ext 取
        if 'mode_ext' in meta and isinstance(meta['mode_ext'], dict):
            key_count = meta['mode_ext'].get('column', 4)
        elif 'column' in meta:
            key_count = meta.get('column', 4)
        content.append(f"CircleSize:{key_count}")  # 自动K数
        content.append("OverallDifficulty:7")
        content.append("ApproachRate:5")
        content.append("SliderMultiplier:1.4")
        content.append("SliderTickRate:1")
        content.append("")
        
        # Events部分（背景图片等）
        content.append("[Events]")
        content.append("//Background and Video events")
        # 使用用户选择的背景图片文件名
        bg_filename = os.path.basename(self.background_file_path.get()) if self.background_file_path.get() else "background.jpg"
        content.append(f'0,0,"{bg_filename}",0,0')
        content.append("//Break Periods")
        content.append("//Storyboard Layer 0 (Background)")
        content.append("//Storyboard Layer 1 (Fail)")
        content.append("//Storyboard Layer 2 (Pass)")
        content.append("//Storyboard Layer 3 (Foreground)")
        content.append("//Storyboard Sound Samples")
        content.append("")
        
        # Timing Points部分
        content.append("[TimingPoints]")
        if bpms:
            for offset, bpm in bpms:
                ms_per_beat = round(60000 / bpm, 2)
                content.append(f"{offset},{ms_per_beat},4,2,0,100,1,0")
        else:
            # 若无time节点，使用默认
            content.append("0,500,4,2,0,100,1,0")
        content.append("")
        
        # Hit Objects部分
        content.append("[HitObjects]")
        
        # Malody列到osu!mania x坐标映射
        column_map = {0: 64, 1: 192, 2: 320, 3: 448}
        max_time_ms = 0
        
        # 转换音符
        for note in notes:
            # 跳过最后一个note（Malody利用最后一个note作为音乐播放事件，若记录会导致osu时长错误）
            column = note.get('column', 0)
            beat = note.get('beat', [0, 0, 1])  # 默认分母为1防止除零
            end_beat = note.get('endbeat')

            # Malody beat: [bar, numerator, denominator]
            # 需要根据 Malody 的 time 节点（offset, bpm）来精确换算每个小节的起始时间
            # 先构建每个 time 节点的绝对时间（ms）和其对应的 bar
            time_points = []
            for t in time:
                t_offset = t.get('offset', 0)
                t_bpm = t.get('bpm', 120)
                t_bar = t.get('bar', 0)
                time_points.append({'bar': t_bar, 'offset': t_offset, 'bpm': t_bpm})

            def beat_to_ms(bar, numerator, denominator):
                """
                将Malody的节拍信息转换为绝对时间（毫秒）。
                根据bar所属的time节点，结合bpm和offset计算。
                """
                if not time_points:
                    return int((bar + numerator / denominator) * 60000 / 120)
                for i in range(len(time_points)-1, -1, -1):
                    if bar + numerator / denominator >= time_points[i]['bar']:
                        tp = time_points[i]
                        break
                else:
                    tp = time_points[0]
                # 计算该拍点距离time节点bar的拍数
                beats_since_tp = (bar + numerator / denominator) - tp['bar']
                ms = tp['offset'] + beats_since_tp * 60000 / tp['bpm']
                return int(ms)

            bar, numerator, denominator = beat
            time_ms = beat_to_ms(bar, numerator, denominator) - global_offset_ms

            x = column_map.get(column, 64)

            if end_beat:
                # 长条note
                end_bar, end_numerator, end_denominator = end_beat if len(end_beat) == 3 else (end_beat[0], end_beat[1], 48)
                end_time_ms = beat_to_ms(end_bar, end_numerator, end_denominator) - global_offset_ms
                content.append(f"{x},192,{time_ms},128,0,{end_time_ms}:0:0:0:0:")
                max_time_ms = max(max_time_ms, end_time_ms)
            else:
                # 普通note
                content.append(f"{x},192,{time_ms},1,0,0:0:0:0:0:")
                max_time_ms = max(max_time_ms, time_ms)
        
        return "\n".join(content)

def main():
    """
    程序入口，根据是否支持拖拽选择Tk类，启动主循环。
    """
    if HAS_DND:
        root = TkinterDnD.Tk()
    else:
        root = tk.Tk()
    
    app = MalodyToOsuConverter(root)
    root.mainloop()

if __name__ == "__main__":
    main()